// Copyright 2022, Uglyhack

extern crate clap;
extern crate geo;
extern crate graphics;
extern crate opengl_graphics;
extern crate piston;
extern crate piston_window;
extern crate rand;

use clap::Parser;
use graphics::color::{GREEN, WHITE};
use graphics::math::Matrix2d;
use graphics::types::Color;
use graphics::{text, Transformed};
use opengl_graphics::{GlGraphics, OpenGL};
use piston::event_loop::{EventSettings, Events};
use piston::input::{RenderArgs, RenderEvent, UpdateArgs, UpdateEvent};
use piston::window::WindowSettings;
use piston::Button::Keyboard;
use piston::{Key, PressEvent};
use piston_window::PistonWindow as Window;
use std::ffi::OsString;
use std::fmt::Write;

mod car;
use car::Car;
mod block;
use block::Block;
mod instructions;
use instructions::Instructions;

const STREET: Color = [0.5, 0.5, 0.5, 1.0];
const STREET_DRAW_SIZE: f64 = 7.;

const DISPLAY_EXTRA_PARKING_WIDTH: f64 = 0.001; // Extra width for parking when displaying to adjust for different timings

#[derive(Parser, Debug)]
struct Cli {
    #[clap(long)]
    optimize: bool,
    #[clap(long, short)]
    file: Option<OsString>,
    #[clap(long, short)]
    variation: Option<f64>,
}

fn main() {
    let cli = Cli::parse();

    let mut instructions = if let Some(filename) = cli.file {
        Instructions::read_from_file(&filename)
    } else {
        Instructions::new()
    };

    if let Some(var) = cli.variation {
        instructions.variation = var;
    }

    if cli.optimize {
        optimize_route_main(instructions);
    } else {
        display_route_main(instructions);
    }
}

/// Container for the application state if in display mode
pub struct App {
    gl: GlGraphics,
    car: Car,
    parking_space: Block,
    zoom: f64,
    speed: f64,
    paused: bool,
    has_failed: bool, // If we have hit an illegal point
    elapsed: f64,
}

impl App {
    fn render(&mut self, args: &RenderArgs) {
        use graphics::*;

        let inside_parking = self.parking_space.contains(&self.car);
        if inside_parking {
            self.paused = true;
        }

        let zoom = self.zoom;

        self.gl.draw(args.viewport(), |c, gl| {
            clear(GREEN, gl);
            let field_transform = field_transform(zoom, args, &c.transform);

            // draw the street
            rectangle(
                STREET,
                [-500., -STREET_DRAW_SIZE, 1000.0, STREET_DRAW_SIZE],
                field_transform,
                gl,
            );

            // Indicate when we park right
            if inside_parking {
                rectangle(WHITE, [0., 0., 20., 20.], c.transform, gl);
            }
        });

        for b in self.parking_space.side_blocks(1000.) {
            if self.has_failed {
                // Draw the red box over the illegal blocks
                b.draw([1., 0., 0., 0.8], self.zoom, args, &mut self.gl);
            }
        }

        self.parking_space
            .draw_parking_space(self.zoom, args, &mut self.gl);
        self.car.draw(self.zoom, args, &mut self.gl);
    }

    fn update(&mut self, args: &UpdateArgs, instructions: &Instructions) {
        if !self.paused {
            self.elapsed += args.dt * self.speed;
            instructions.apply(&mut self.car, self.elapsed);
            self.car.step(args.dt * self.speed);
        }

        // Check for intersection
        for b in self.parking_space.side_blocks(1000.) {
            if b.intersects(&self.car) {
                self.has_failed = true;
            }
        }
    }
}

/// Create a transform for the local view (with center and zoom)
fn field_transform(zoom: f64, args: &RenderArgs, transform: &Matrix2d) -> Matrix2d {
    let (middle_x, middle_y) = (args.window_size[0] / 3.0, args.window_size[1] / 2.0);
    transform
        .trans(middle_x, middle_y) // Move to the center (last action)
        .scale(zoom, zoom) // Scale to zoom
}

/// Run the optimizer, randomly trying things until we get a better route
fn optimize_route_main(mut instructions: Instructions) {
    let mut parking_space = Block::new_parking_space();

    loop {
        optimize_instructions(&mut instructions, &mut parking_space);
        println!("Instructions:");
        instructions.print_all();
        println!("\n");
    }
}

/// Try a number of random variations to find one that improves inststructions
fn optimize_instructions(instructions: &mut Instructions, space: &mut Block) {
    // From a set of 100, find the best instructions
    let mut best_score = run_test(space, instructions);
    for i in 0..250 {
        let mut instructions_test = instructions.clone();
        if i == 0 {
            instructions_test.optimize_reverse();
        } else {
            instructions_test.mutate();
        }
        let score = run_test(space, &instructions_test);
        if score > best_score {
            best_score = score;
            *instructions = instructions_test;
        }
    }
    println!("Score: {}", best_score);
}

// Run the simulation, indicating if successfully parked
fn run_test(space: &Block, instructions: &Instructions) -> f64 {
    let mut time = 0.0;
    let dt = 0.0083333333333 * 4.; // Typical for visual simulation
    let mut car = Car::new();
    instructions.setup(&mut car);

    loop {
        // Check if we've been going to long
        if time >= 120.0 {
            return -1000.;
        }
        // Update the time
        time += dt;
        instructions.apply(&mut car, time);

        // Run the simulations
        car.step(dt);

        // Check if  crossing next to spot
        for b in space.side_blocks(1000.) {
            if b.intersects(&car) {
                return -1000.;
            }
        }

        // And check if we were successfull
        let inside_parking = space.contains(&car);
        if inside_parking {
            return car.top;
        }
    }
}

/// Show how the route is run
fn display_route_main(instructions: Instructions) {
    let opengl = OpenGL::V3_2;

    let mut window: Window = WindowSettings::new("Car simulator", [200, 200])
        .graphics_api(opengl)
        .exit_on_esc(true)
        .build()
        .unwrap();

    let mut app = App {
        gl: GlGraphics::new(opengl),
        car: Car::new(),
        parking_space: Block::new_parking_space(),
        zoom: 25.0,
        speed: 4.0,
        paused: false,
        has_failed: false,
        elapsed: 0.,
    };

    let mut events = Events::new(EventSettings::new());
    let mut glyphs = window
        .load_font("/usr/share/fonts/TTF/AdventPro-Bold.ttf")
        .unwrap();

    let mut return_string = String::new();

    instructions.setup(&mut app.car);
    if instructions.has_steps() {
        // We are running instructions, so we want to start paused
        // This is to make it easier to screencapture
        app.paused = true;
    }

    // Because this is display, we want to be a little more generous with the parking space
    // Otherwise we get orange just due to different rounding
    app.parking_space.left -= DISPLAY_EXTRA_PARKING_WIDTH;
    app.parking_space.width += 2. * DISPLAY_EXTRA_PARKING_WIDTH;

    while let Some(e) = events.next(&mut window) {
        if let Some(args) = e.render_args() {
            app.render(&args);
            // Draw the text from return_string
            // I couldn't figure out how to draw text using the gl object only, so I just copied
            // this from an example.
            window.draw_2d(&e, |c, g, device| {
                let transform = c.transform.trans(10.0, 100.0);
                text::Text::new_color([0., 0., 1., 1.], 32)
                    .draw(&return_string, &mut glyphs, &c.draw_state, transform, g)
                    .unwrap();
                glyphs.factory.encoder.flush(device);
            });
        }

        if let Some(args) = e.update_args() {
            return_string.clear();
            write!(return_string, "{}", -app.car.top).unwrap();
            app.update(&args, &instructions);
        }

        if let Some(button) = e.press_args() {
            match button {
                Keyboard(Key::Z) => app.speed /= 1.5,
                Keyboard(Key::X) => app.speed *= 1.5,
                Keyboard(Key::R) => app.car.reverse ^= true,
                Keyboard(Key::A) => app.zoom /= 1.1,
                Keyboard(Key::S) => app.zoom *= 1.1,
                Keyboard(Key::Q) => app.car.w -= 0.05,
                Keyboard(Key::W) => app.car.w += 0.05,
                Keyboard(Key::Space) => app.paused ^= true,

                _ => {}
            }
        }
    }
}
